﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Diagnostics.Contracts;

namespace SimpleData.Infrastructure.Utils
{

	/// <summary>
	/// Auxilia na construção das expressões LINQ
	/// </summary>
	public class ExpressionBuilder
	{
		private Dictionary<String, ParameterExpression> variables = new Dictionary<String, ParameterExpression>( StringComparer.InvariantCultureIgnoreCase );

		#region [ .ctor ]

		/// <summary>
		/// 
		/// </summary>
		public ExpressionBuilder()
			: this( null )
		{ }

		/// <summary>
		/// 
		/// </summary>
		/// <param name="initialVars">Variáveis iniciais</param>
		public ExpressionBuilder( IEnumerable<KeyValuePair<String, ParameterExpression>> initialVars )
		{
			this.Expressions = new List<Expression>();

			if ( initialVars != null )
				initialVars.Each( pair => this.variables.Add( pair.Key, pair.Value ) );
		}

		#endregion

		/// <summary>
		/// Obtém ou define a lista de expressão que foram montadas até o momento
		/// </summary>
		protected List<Expression> Expressions
		{
			get;
			private set;
		}

		/// <summary>
		/// Obtém as variáveis que foram declaradas durante a construção da expressão 
		/// </summary>
		public Dictionary<String, ParameterExpression> Variables
		{
			get { return this.variables; }
		}

		/// <summary>
		/// Realiza a construção do block de expressões
		/// </summary>
		/// <returns></returns>
		public Expression Build()
		{
			if ( this.Expressions.Count == 1 )
				return this.Expressions[ 0 ];
			else if ( this.Expressions.Count > 1 )
				return Expression.Block( this.variables.Values, this.Expressions );
			else
				return null;
		}

		#region [ DeclareVar Methods ]

		/// <summary>
		/// Associa o valor da expressão para a variável <paramref name="leftVar"/>
		/// </summary>
		/// <typeparam name="TInput"></typeparam>
		/// <param name="leftVar"></param>
		/// <param name="rightVar"></param>
		public void Assign<TInput>( ParameterExpression leftVar, Expression<Func<TInput, object>> rightVar )
		{
			Contract.Requires<ArgumentNullException>( leftVar != null, "leftVar" );
			Contract.Requires<ArgumentNullException>( rightVar != null, "rightVar" );

			Expression value = rightVar.RemoveLambda();

			if ( leftVar.Type != rightVar.ReturnType )
				value = Expression.Convert( value, leftVar.Type );
			this.Include( Expression.Assign( leftVar, value ) );
		}

		public ParameterExpression DeclareVar<TVar>( string name )
		{
			return this.DeclareVar( name, typeof( TVar ) );
		}

		public ParameterExpression DeclareVar<TVar>( string name, TVar initialValue )
		{
			return this.DeclareVar( name, typeof( TVar ), initialValue );
		}

		public ParameterExpression DeclareVar( string name, Type varType )
		{
			return this.DeclareVar( name, varType, ( object )null );
		}

		public ParameterExpression DeclareVar( string name, Type varType, object initialValue )
		{
			Contract.Requires<ArgumentNullException>( name != null, "name" );
			Contract.Requires<ArgumentException>( !this.Variables.ContainsKey( name ), "A variável já foi declarada nesse contexto." );

			// Declara a variável
			var variable = Expression.Variable( varType, name );
			this.variables.Add( name, variable );

			// Inicializa
			if ( initialValue != null )
				this.Expressions.Add( Expression.Assign( variable, Expression.Constant( initialValue, varType ) ) );

			return variable;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="TVar"></typeparam>
		/// <param name="name"></param>
		/// <param name="getter"></param>
		/// <returns></returns>
		//public ParameterExpression DeclareVar<TVar>( string name, Expression<Func<TVar>> getter )
		//{
		//    Contract.Requires<ArgumentNullException>( getter != null, "getter" );

		//    // Declara a variável
		//    var variable = this.DeclareVar<TVar>( name );
		//    // Inicializa
		//    this.Include( Expression.Assign( variable, getter.RemoveLambda() ) );
		//    return variable;
		//}

		public ParameterExpression DeclareVar<TVar, TInput>( string name, Expression<Func<TInput, TVar>> initializer )
		{
			Contract.Requires<ArgumentNullException>( initializer != null, "initializer" );

			// Declara a variável
			var variable = this.DeclareVar<TVar>( name );

			// Inicializa
			this.Include( Expression.Assign( variable, initializer.RemoveLambda() ) );
			return variable;
		}

		#endregion

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="TInput"></typeparam>
		/// <param name="condition"></param>
		/// <param name="thenPart"></param>
		/// <param name="elsePart"></param>
		public void If<TInput>( Expression<Func<TInput, bool>> condition, Expression thenPart, Expression elsePart )
		{
			if ( elsePart == null )
				this.Include( Expression.IfThen( condition.RemoveLambda(), thenPart ) );
			else
				this.Include( Expression.IfThenElse( condition.RemoveLambda(), thenPart, elsePart ) );
		}


		/// <summary>
		/// Adiciona uma nova expressão
		/// </summary>
		/// <param name="exp"></param>
		protected void Include( Expression exp )
		{
			if ( exp != null )
				// Passa a expressão pelos visitors
				this.Expressions.Add( exp.ResolveExternals( this.variables ) );
		}
	}
}
